<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\Jwt;

use DomainException;
use InvalidArgumentException;
use UnexpectedValueException;

class JWK
{
    /**
     * @param array $jwkList
     * @param array $pemList
     * @return array
     * Parse a set of JWK keys
     */
    public static function parseKeySet(array $jwkList, array &$pemList):array
    {
        $keyList    = [];
        $pemList    = [];
        if(!isset($jwkList['keys'])){
            throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
        }
        if(empty($jwkList['keys'])){
            throw new InvalidArgumentException('JWK Set did not contain any keys');
        }
        foreach($jwkList['keys'] as $k => $v){
            $kid            = $v['kid']??$k;
            $pemString      = '';
            if($key = self::parseKey($v, $pemString)){
                $keyList[$kid]  = $key;
                $pemList[$kid]  = $pemString;
            }
        }
        if(count($keyList) < 1){
            throw new UnexpectedValueException('No supported algorithms found in JWK Set');
        }
        return $keyList;
    }

    /**
     * @param array $jwk
     * @param string $pemString
     * @return resource
     * Parse a JWK key
     */
    private static function parseKey(array $jwk, string &$pemString)
    {
        if(empty($jwk)){
            throw new InvalidArgumentException('JWK must not be empty');
        }
        if(!isset($jwk['kty'])){
            throw new UnexpectedValueException('JWK must contain a "kty" parameter');
        }
        switch($jwk['kty']){
            case 'RSA': {
                if(array_key_exists('d', $jwk)){
                    throw new UnexpectedValueException('RSA private keys are not supported');
                }
                if(!isset($jwk['n']) || !isset($jwk['e'])){
                    throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
                }
                $pemString  = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
                $publicKey  = openssl_pkey_get_public($pemString);
                if(false === $publicKey){
                    throw new DomainException('OpenSSL error: ' . openssl_error_string());
                }
                return $publicKey;
            }
            default:{
                throw new InvalidArgumentException('Currently only RSA is supported');
            }
        }
    }

    /**
     * @param string $n
     * @param string $e
     * @return string
     * Create a public key represented in PEM format from RSA modulus and exponent information
     */
    private static function createPemFromModulusAndExponent(string $n, string $e):string
    {
        $modulus        = JWT::urlsafeB64Decode($n);
        $publicExponent = JWT::urlsafeB64Decode($e);
        $components     = [
            'modulus'           => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus),
            'publicExponent'    => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent),
        ];
        $rsaPublicKey   = pack('Ca*a*a*', 48, self::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent']);
        $rsaOID         = pack('H*', '300d06092a864886f70d0101010500');
        $rsaPublicKey   = chr(0).$rsaPublicKey;
        $rsaPublicKey   = chr(3).self::encodeLength(strlen($rsaPublicKey)).$rsaPublicKey;
        $rsaPublicKey   = pack('Ca*a*', 48, self::encodeLength(strlen($rsaOID . $rsaPublicKey)), $rsaOID.$rsaPublicKey);
        return "-----BEGIN PUBLIC KEY-----\r\n".chunk_split(base64_encode($rsaPublicKey), 64).'-----END PUBLIC KEY-----';
    }

    /**
     * @param int $length
     * @return string
     * DER-encode the length
     */
    private static function encodeLength(int $length):string
    {
        if($length <= 0x7F) return chr($length);
        $temp   = ltrim(pack('N', $length), chr(0));
        return pack('Ca*', 0x80 | strlen($temp), $temp);
    }
}